<?php
class Service_HtmlParser {
	const PRINATABLE  = 0x1;
	const ALPHA       = 0x2;
	const LAT         = 0x4;
	const RUS         = 0x8;
	const NUMERIC     = 0x10;
	const SPACE       = 0x20;
	const NAME        = 0x40;
	const URL         = 0x100;
	const NOPRINT     = 0x200;
	const PUNCTUATUON = 0x400;
	//const           = 0x800;
	//const           = 0x1000;
	const HTML_QUOTE  = 0x2000;
	const TAG_QUOTE   = 0x4000;
	const QUOTE_CLOSE = 0x8000;
	const NL          = 0x10000;
	const QUOTE_OPEN  = 0;

	const STATE_TEXT = 0;
	const STATE_TAG_PARAMS = 1;
	const STATE_TAG_PARAM_VALUE = 2;
	const STATE_INSIDE_TAG = 3;
	const STATE_INSIDE_NOTEXT_TAG = 4;
	const STATE_INSIDE_PREFORMATTED_TAG = 5;

	/**
	 * Константы для класификации тегов
	 *
	 */
	const TR_TAG_ALLOWED = 1; 		 // Тег позволен
	const TR_PARAM_ALLOWED = 2; 	 // Параметр тега позволен (a->title, a->src, i->alt)
	const TR_PARAM_REQUIRED = 3; 	 // Параметр тега влятся необходимым (a->href, img->src)
	const TR_TAG_SHORT = 4;   		 // Тег может быть коротким (img, br)
	const TR_TAG_CUT = 5;			 // Тег необходимо вырезать вместе с контентом (script, iframe)
	const TR_TAG_CHILD = 6;			 // Тег может содержать другие теги
	const TR_TAG_CONTAINER = 7;      // Тег может содержать лишь указанные теги. В нём не может быть текста
	const TR_TAG_CHILD_TAGS = 8;	 // Теги которые может содержать внутри себя другой тег
	const TR_TAG_PARENT = 9;		 // Тег в котором должен содержаться данный тег
	const TR_TAG_PREFORMATTED = 10;	 // Преформатированные тег, в котором всё заменяется на HTML сущности типа <pre> сохраняя все отступы и пробелы
	const TR_PARAM_AUTO_ADD = 11;    // Auto add parameters + default values (a->rel[=nofollow])
	const TR_TAG_NO_TYPOGRAPHY = 12; // Отключение типографирования для тега
	const TR_STYLE_ALLOWED = 13; 	 // Параметр тега позволен (a->title, a->src, i->alt)

  protected $curPos;     // Позиция, в которой мы сейчас находимся
	protected $curCh;      // Текущий символ
	protected $curChOrd;   // Числовое значение текущего символа
	protected $curChClass; // Класс текущего символа
	protected $states;     // Состояние, в котором мы сейчас находимся
	protected $state;      // Положение в котором мы находимся в коде

	protected $text;         //Исходный текст
	protected $textBuf;      // Буфер с текстом
	protected $textLen = 0;  // Длина текста
	protected $tagsStack;	   // Стек тегов
	protected $openedTag;    // Открытый в текущий момент тег
	protected $isAutoBrMode = false; // \n = <br/>
	protected $isAutoLinkMode = true;
	protected $br = "<br/>";
	protected $noTypoMode = false;

	public $tagsRules = array();
	public $entities0 = array('"'=>'&quot;', "'"=>'&#39;', '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;');
	public $entities1 = array();
	public $entities2 = array('<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;');
	public $textQuotes = array(array('«', '»'), array('„', '“'));
	public $dash = " — ";
	public $apostrof = "’";
	public $dotes = "…";
	public $nl = "\r\n";
	public $defaultTagParamRules = array('href' => '#link', 'src' => '#image', 'width' => '#int', 'height' => '#int', 'text' => '#text', 'title' => '#text');



	public    $outBuffer = '';
	public    $errors;

	/**
	 * Классы символов генерируются symclass.php
	 *
	 * @var array
	 */
	protected $chClasses = array(0=>512,1=>512,2=>512,3=>512,4=>512,5=>512,6=>512,7=>512,8=>512,9=>32,10=>66048,11=>512,12=>512,13=>66048,14=>512,15=>512,16=>512,17=>512,18=>512,19=>512,20=>512,21=>512,22=>512,23=>512,24=>512,25=>512,26=>512,27=>512,28=>512,29=>512,30=>512,31=>512,32=>32,97=>71,98=>71,99=>71,100=>71,101=>71,102=>71,103=>71,104=>71,105=>71,106=>71,107=>71,108=>71,109=>71,110=>71,111=>71,112=>71,113=>71,114=>71,115=>71,116=>71,117=>71,118=>71,119=>71,120=>71,121=>71,122=>71,65=>71,66=>71,67=>71,68=>71,69=>71,70=>71,71=>71,72=>71,73=>71,74=>71,75=>71,76=>71,77=>71,78=>71,79=>71,80=>71,81=>71,82=>71,83=>71,84=>71,85=>71,86=>71,87=>71,88=>71,89=>71,90=>71,1072=>11,1073=>11,1074=>11,1075=>11,1076=>11,1077=>11,1078=>11,1079=>11,1080=>11,1081=>11,1082=>11,1083=>11,1084=>11,1085=>11,1086=>11,1087=>11,1088=>11,1089=>11,1090=>11,1091=>11,1092=>11,1093=>11,1094=>11,1095=>11,1096=>11,1097=>11,1098=>11,1099=>11,1100=>11,1101=>11,1102=>11,1103=>11,1040=>11,1041=>11,1042=>11,1043=>11,1044=>11,1045=>11,1046=>11,1047=>11,1048=>11,1049=>11,1050=>11,1051=>11,1052=>11,1053=>11,1054=>11,1055=>11,1056=>11,1057=>11,1058=>11,1059=>11,1060=>11,1061=>11,1062=>11,1063=>11,1064=>11,1065=>11,1066=>11,1067=>11,1068=>11,1069=>11,1070=>11,1071=>11,48=>337,49=>337,50=>337,51=>337,52=>337,53=>337,54=>337,55=>337,56=>337,57=>337,34=>57345,39=>16385,46=>1281,44=>1025,33=>1025,63=>1281,58=>1025,59=>1281,1105=>11,1025=>11,47=>257,38=>257,37=>257,45=>257,95=>257,61=>257,43=>257,35=>257,124=>257,);




	/**
	 * Получение следующего символа из входной строки
	 * @return string считанный символ
	 */
	protected function getCh(){
		return $this->goToPosition($this->curPos+1);
	}

	/**
	 * Перемещение на указанную позицию во входной строке и считывание символа
	 * @return string символ в указанной позиции
	 */
	protected function goToPosition($position){
		$this->curPos = $position;
		if($this->curPos < $this->textLen){
			$this->curCh = $this->textBuf[$this->curPos];
			$this->curChOrd = uniord($this->curCh);
			$this->curChClass = $this->getCharClass($this->curChOrd);
		} else {
			$this->curCh = null;
			$this->curChOrd = 0;
			$this->curChClass = 0;
		}
		return $this->curCh;
	}

	/**
	 * Возвращает класс символа
	 *
	 * @return int
	 */
	protected function getCharClass($ord){
		return isset($this->chClasses[$ord]) ? $this->chClasses[$ord] : self::PRINATABLE;
	}


	/**
	 * Сохранить текущее состояние
	 *
	 * @return int - поозиция, рисвоенная текущему состоянию в стеке состояний
	 */
	protected function saveState(){
		$state = array(
			'pos'   => $this->curPos,
			'ch'    => $this->curCh,
			'ord'   => $this->curChOrd,
			'class' => $this->curChClass,
		);

		$this->states[] = $state;
		return count($this->states)-1;
	}


	/**
	 * Восстановление состояния из стека
	 *
	 * @param int $index - номер состояния, к которому возвращаемся
	 *
	 */
	protected function restoreState($index = null){
		if(!count($this->states)) throw new Exception('Конец стека');
		if($index == null){
			$state = array_pop($this->states);
		} else {
			if(!isset($this->states[$index])) throw new Exception('Неверный индекс стека');
			$state = $this->states[$index];
			$this->states = array_slice($this->states, 0, $index);
		}

		$this->curPos     = $state['pos'];
		$this->curCh      = $state['ch'];
		$this->curChOrd   = $state['ord'];
		$this->curChClass = $state['class'];
	}

	/**
	 * Метод преобразовывает строку к массиву
	 *
	 * @param string $str
	 * @return array
	 */
	protected function &strToArray($str){
		$chars = null;
		preg_match_all('/./su', $str, $chars);
		return $chars[0];
	}


	/**
	 * Проверяет точное вхождение символа в текущей позиции
	 * Если символ соответствует указанному автомат сдвигается на следующий
	 *
	 * @param string $ch
	 * @return boolean
	 */
	protected function matchCh($ch, $skipSpaces = false){
		if($this->curCh == $ch) {
			$this->getCh();
			if($skipSpaces) $this->skipSpaces();
			return true;
		}

		return false;
	}

	/**
	 * Проверяет точное вхождение символа указанного класса в текущей позиции
	 * Если символ соответствует указанному классу автомат сдвигается на следующий
	 *
	 * @param int $chClass класс символа
	 * @return string найденый символ или false
	 */
	protected function matchChClass($chClass, $skipSpaces = false){
		if(($this->curChClass & $chClass) == $chClass) {
			$ch = $this->curCh;
			$this->getCh();
			if($skipSpaces) $this->skipSpaces();
			return $ch;
		}

		return false;
	}

	/**
	 * Проверка на точное совпадение строки в текущей позиции
	 * Если строка соответствует указанной автомат сдвигается на следующий после строки символ
	 *
	 * @param string $str
	 * @return boolean
	 */
	protected function matchStr($str, $skipSpaces = false){
		$this->saveState();
		$len = strlen($str);
		$test = '';
		while($len-- && $this->curChClass){
			$test.=$this->curCh;
			$this->getCh();
		}

		if($test == $str) {
			if($skipSpaces) $this->skipSpaces();
			return true;
		} else {
			$this->restoreState();
			return false;
		}
	}


	/**
	 * Пропуск текста до нахождения указанной строки или символа
	 *
	 * @param string $str строка или символ ля поиска
	 * @return boolean
	 */
	protected function skipUntilStr($str){
		$str = $this->strToArray($str);
		$firstCh = $str[0];
		$len = count($str);
		while($this->curChClass){
			if($this->curCh == $firstCh){
				$this->saveState();
				$this->getCh();
				$strOK = true;
				for($i = 1; $i<$len ; $i++){
					// Конец строки
					if(!$this->curChClass){
						return false;
					}
					// текущий символ не равен текущему символу проверяемой строки?
					if($this->curCh != $str[$i]){
						$strOK = false;
						break;
					}
					// Следующий символ
					$this->getCh();
				}

				// При неудаче откатываемся с переходим на следующий символ
				if(!$strOK){
					$this->restoreState();
				} else {
					return true;
				}
			}
			// Следующий символ
			$this->getCh();
		}
		return false;
	}

	/**
	 * Пропуск пробелов
	 *
	 * @param int $count - начальное число пробелов. Передается по ссылке
	 * @return bool - были ли пробелы
	 */
	protected function skipSpaces(&$count = 0){
		while($this->curChClass == self::SPACE) {
			$this->getCh();
			$count++;
		}
		return $count > 0;
	}

	/**
	 * Пропуск текста до нахождения указанного символа
	 *
	 * @param string $ch сиимвол
	 * @return string найденый символ или false
	 */
	protected function skipUntilCh($ch){
		$chPos = strpos($this->text, $ch, $this->curPos);
		if($chPos){
			return $this->goToPosition($chPos);
		} else {
			return false;
		}
	}

	/**
	 * Пропуск переводов строк подсчет кол-ва
	 *
	 * @param int $count ссылка для возвращения числа переводов строк
	 * @return boolean
	 */
	protected function skipNL(&$count = 0){
		if(!($this->curChClass & self::NL)) return false;
		$count++;
		$firstNL = $this->curCh;
		$nl = $this->getCh();
		while($this->curChClass & self::NL){
			// Если символ новый строки ткой же как и первый увеличиваем счетчик
			// новых строк. Это сработает при любых сочетаниях
			// \r\n\r\n, \r\r, \n\n - две перевода
			if($nl == $firstNL) $count++;
			$nl = $this->getCh();
			// Между переводами строки могут встречаться пробелы
			$this->skipSpaces();
		}
		return true;
	}

	/**
	 * Метод проверяет, встретился ли нам комментарий, и если он встретился, пропускает его
	 *
	 * @return bool - флаг, встретился ли комментарий
	 */
	protected function comment(){
		if(!$this->matchStr('<!--')) return false;
		return $this->skipUntilStr('-->');
	}

	/**
	 * Метод делает парсинг html-строки
	 *
	 * @param string $text - html-строка для парсинга
	 * @param array $errors - массив с ошибками, которые формирует строка
	 * @return string
	 */
	function parse($text, &$errors){
		$this->curPos = -1;
		$this->curCh = null;
		$this->curChOrd = 0;
		$this->state = self::STATE_TEXT;
		$this->states = array();
		$this->quotesOpened = 0;

		// Авто растановка BR?
		if($this->isAutoBrMode) {
			/*$this->text = preg_replace('/<br\/?>(\r\n|\n\r|\n)?/ui', $this->nl, $text);*/
			$this->text = preg_replace('/<br\/?>(\r\n|\n\r|\n)?/ui', '', $text);
		} else {
			//$this->text = $text;
			$this->text = preg_replace('/(\r\n|\n\r|\n)+/ui', '*NR*', $text);
		}


		//if(!empty($this->autoReplace)){
			//$this->text = str_replace($this->autoReplace['from'], $this->autoReplace['to'], $this->text);
		//}
		$this->textBuf = $this->strToArray($this->text);
		$this->textLen = count($this->textBuf);
		$this->getCh();
		$content = '';
		$this->outBuffer='';
		$this->brAdded=0;
		$this->tagsStack = array();
		$this->openedTag = null;
		$this->errors = array();
		$this->skipSpaces();
		$this->anyThing($content);
		$errors = $this->errors;
		$content = str_replace("*NR*", "\n", $content);
		return $content;
	}

	/**
	 * Метод формирует строку тега
	 *
	 * @param string $tag - название тега
	 * @param array $params - параметры
	 * @param unknown_type $content
	 * @param bool $short
	 * @param string $parentTag - родительский тег
	 * @return string
	 */
	protected function makeTag($tag, $params, $content, $short, $parentTag = null){
		$tag = strtolower($tag);

		// Получаем правила фильтрации тега
		$tagRules = isset($this->tagsRules[$tag]) ? $this->tagsRules[$tag] : null;
    $tagRules[2]['class'] = 1;
		// Проверка - родительский тег - контейнер, содержащий только другие теги (ul, table, etc)
		$parentTagIsContainer = $parentTag && isset($this->tagsRules[$parentTag][self::TR_TAG_CONTAINER]);

		// Вырезать тег вместе с содержанием
		if ($tagRules && isset($this->tagsRules[$tag][self::TR_TAG_CUT])) return '';

		// Позволен ли тег
		if (!$tagRules || empty($tagRules[self::TR_TAG_ALLOWED])) return $parentTagIsContainer ? '' : $content;

		// Если тег находится внутри другого - может ли он там находится?
		if ($parentTagIsContainer){
			if (!isset($this->tagsRules[$parentTag][self::TR_TAG_CHILD_TAGS][$tag])) return '';
		}

		// Тег может находится только внтури другого тега
		if (isset($tagRules[self::TR_TAG_CHILD])){
			if (!isset($tagRules[self::TR_TAG_PARENT][$parentTag])) return $content;
		}


		$resParams = array();
		foreach ($params as $param => $value) {
			$param = strtolower($param);
			if ($param == 'style') {
			  $styleText = '';
			  if ($value && is_array($value)) {
    	    foreach ($value as $styleParamName => $styleParamValue) {
    	  	  // Ghверяем, допустим ли такой стиль для тега
    	      $styleAllowedValues = isset($tagRules[self::TR_STYLE_ALLOWED][$styleParamName]) ? $tagRules[self::TR_STYLE_ALLOWED][$styleParamName] : false;
			      if(empty($styleAllowedValues)) continue;
    	      $styleText .= $styleParamName . ':' . $styleParamValue . ';';
    	    }
    	    if (!empty($styleText)) $resParams[$param] = $styleText;
    	  }
    	  continue;

			}
			$value = trim($value);
			if(empty($value)) continue;

			// Атрибут тега разрешён? Какие возможны значения? Получаем список правил
			$paramAllowedValues = isset($tagRules[self::TR_PARAM_ALLOWED][$param]) ? $tagRules[self::TR_PARAM_ALLOWED][$param] : false;
			if(empty($paramAllowedValues)) continue;

			// Если есть список разрешённых параметров тега
			if(is_array($paramAllowedValues) && !in_array($value, $paramAllowedValues)) {
				$this->eror("Недопустимое значение для атрибута тега $tag $param=$value");
				continue;
			// Если атрибут тега помечен как разрешённый, но правила не указаны - смотрим в массив стандартных правил для атрибутов
			} elseif($paramAllowedValues === true && !empty($this->defaultTagParamRules[$param])){
				$paramAllowedValues = $this->defaultTagParamRules[$param];
			}

			if(is_string($paramAllowedValues)){
				switch($paramAllowedValues){
					case '#int':
						if(!is_numeric($value)) {
							$this->eror("Недопустимое значение для атрибута тега $tag $param=$value. Ожидалось число");
							continue(2);
						}
						break;

					case '#text':
						$value = htmlspecialchars($value);
						break;

					case '#link':
						// Ява-скрипт в ссылке
						if(preg_match('/javascript:/ui', $value)) {
							$this->eror('Попытка вставить JavaScript в URI');
							continue(2);
						}
						// Первый символ должен быть a-z0-9!
						if(!preg_match('/^[a-z0-9\/]/ui', $value)) {
							$this->eror('URI: Первый символ адреса должен быть буквой или цифрой');
							continue(2);
						}
						// HTTP в начале если нет
						if(!preg_match('/^(http|https|ftp):\/\//ui', $value) && !preg_match('/^\//ui', $value)) $value = 'http://'.$value;
						break;

					case '#image':
						// Ява-скрипт в пути к картинке
						if(preg_match('/javascript:/ui', $value)) {
							$this->eror('Попытка вставить JavaScript в пути к изображению');
							continue(2);
						}
						// HTTP в начале если нет
						if(!preg_match('/^http:\/\//ui', $value) && !preg_match('/^\//ui', $value)) $value = 'http://'.$value;
						break;

					default:
						$this->eror("Неверное описание атрибута тега в настройке Jevix: $param => $paramAllowedValues");
						continue(2);
						break;
				}
			}

			$resParams[$param] = $value;
		}

		// Проверка обязятельных параметров тега
		// Если нет обязательных параметров возвращаем только контент
		$requiredParams = isset($tagRules[self::TR_PARAM_REQUIRED]) ? array_keys($tagRules[self::TR_PARAM_REQUIRED]) : array();
		if($requiredParams){
			foreach($requiredParams as $requiredParam){
				if(empty($resParams[$requiredParam])) return $content;
			}
		}

		// Автодобавляемые параметры
		if(!empty($tagRules[self::TR_PARAM_AUTO_ADD])){
	        foreach($tagRules[self::TR_PARAM_AUTO_ADD] as $name => $value) {
	            // If there isn't such attribute or it has wrong value - setup it
	            if(!array_key_exists($name, $resParams) || $resParams[$name] != $value) {
	                $resParams[$name] = $value;
	            }
	        }
		}

		// Пустой некороткий тег удаляем
		if(!$short && empty($content)) return '';
		// Собираем тег
		$text='<'.$tag;
		// Параметры
		foreach($resParams as $param=>$value) $text.=' '.$param.'="'.$value.'"';
		// Закрытие тега (если короткий то без контента)
		//$text.= $short && $this->isXHTMLMode ? '/>' : '>';
		$text.= $short ? '/>' : '>';
		if(isset($tagRules[self::TR_TAG_CONTAINER])) $text .= "\r\n";
		if(!$short) $text.= $content.'</'.$tag.'>';
		if($parentTagIsContainer) $text .= "\r\n";
		if($tag == 'br') $text.="\r\n";
		return $text;
	}

  /**
   * Метод начинает разбор html-текста
   *
   * @param string $content
   * @param string $parentTag
   * @return bool
   */
	protected function anyThing(&$content = '', $parentTag = null){
		$this->skipNL();
		while ($this->curChClass) {
			$tag = '';
			$params = null;
			$text = null;
			$shortTag = false;
			$name = null;

			// Если мы находимся в режиме тега без текста
			// пропускаем контент пока не встретится <
			if ($this->state == self::STATE_INSIDE_NOTEXT_TAG && $this->curCh!='<') {
				$this->skipUntilCh('<');
			}

			// Если нам встретился тег
			if ($this->curCh == '<' && $this->tag($tag, $params, $text, $shortTag)) {

			  // Преобразуем тег в текст
				$tagText = $this->makeTag($tag, $params, $text, $shortTag, $parentTag);
				$content .= $tagText;
				// Пропускаем пробелы после <br> и запрещённых тегов, которые вырезаются парсером
				if ($tag=='br') {
					$this->skipNL();
				}
				elseif (empty($tagText)) {
					$this->skipSpaces();
				}

			// Коментарий <!-- -->
			} elseif($this->curCh == '<' && $this->comment()){
				continue;

			// Конец тега или символ <
			} elseif($this->curCh == '<') {
				// Если встречается <, но это не тег
				// то это либо закрывающийся тег либо знак <
				$this->saveState();
				if ($this->tagClose($name)) {
					// Если это закрывающийся тег, то мы делаем откат
					// и выходим из функции
					// Но если мы не внутри тега, то просто пропускаем его
					if($this->state == self::STATE_INSIDE_TAG || $this->state == self::STATE_INSIDE_NOTEXT_TAG) {
						$this->restoreState();
						return false;
					} else {
						$this->eror('Не ожидалось закрывающегося тега ' . $name);
					}
				} else {
					if($this->state != self::STATE_INSIDE_NOTEXT_TAG) $content .= $this->entities2['<'];
					$this->getCh();
				}

			// Текст
			} elseif ($this->text($text)){
				$content .= $text;
			}
		}

		return true;
	}

  /**
   * Метод обрабатывает тег
   *
   * @param string $tag - ссылка для сохранения названия тега
   * @param array $params - дополнительные параметры
   * @param string $content - указатель на строку, формирующую результат
   * @param bool $short - указатель для сохранения флага, может ли быть тег коротким
   * @return bool - флаг, показывающий, удачно ли прошла обработка тега
   */
	protected function tag(&$tag, &$params, &$content, &$short){
		$this->saveState();
		$params = array();
		$tag = '';
		$closeTag = '';
		$params = array();
		$short = false;
		if (!$this->tagOpen($tag, $params, $short)) return false;
		// Короткая запись тега
		if ($short) return true;

		// Сохраняем кавычки и состояние
		  //$oldQuotesopen = $this->quotesOpened;
		$oldState = $this->state;
		//$oldNoTypoMode = $this->noTypoMode;
		  //$this->quotesOpened = 0;


		// Если в теге не должно быть текста, а только другие теги
		// Переходим в состояние self::STATE_INSIDE_NOTEXT_TAG
		if (!empty($this->tagsRules[$tag][self::TR_TAG_PREFORMATTED])){
			$this->state = self::STATE_INSIDE_PREFORMATTED_TAG;
		} elseif(!empty($this->tagsRules[$tag][self::TR_TAG_CONTAINER])){
			$this->state = self::STATE_INSIDE_NOTEXT_TAG;
		} elseif(!empty($this->tagsRules[$tag][self::TR_TAG_NO_TYPOGRAPHY])) {
			//$this->noTypoMode = true;
			$this->state = self::STATE_INSIDE_TAG;
		} else {
			$this->state = self::STATE_INSIDE_TAG;
		}

		// Контент тега
		array_push($this->tagsStack, $tag);
		$this->openedTag = $tag;
		$content = '';
		//if($this->state == self::STATE_INSIDE_PREFORMATTED_TAG){
			//$this->preformatted($content, $tag);
		//} else {
			$this->anyThing($content, $tag);
		//}

		array_pop($this->tagsStack);
		$this->openedTag = !empty($this->tagsStack) ? array_pop($this->tagsStack) : null;

		$isTagClose = $this->tagClose($closeTag);
		if($isTagClose && ($tag != $closeTag)) {
			$this->eror("Неверный закрывающийся тег $closeTag. Ожидалось закрытие $tag");
			//$this->restoreState();
		}

		// Восстанавливаем предыдущее состояние и счетчик кавычек
		$this->state = $oldState;
		//$this->noTypoMode = $oldNoTypoMode;
		  //$this->quotesOpened = $oldQuotesopen;

		return true;
	}

	/**
	 * Метод проверяет, корректно ли введен тег и сохраняет необходимые данные
	 *
	 * @param string $name - ссылка для сохранения имени тега
	 * @param unknown_type $params
	 * @param bool $short - ссылка для сохранения флага, указывающего, разрешен ли для тега короткий формат
	 * @return bool - флаг, показывающий, удалось ли получить корректный тег
	 */
  protected function tagOpen(&$name, &$params, &$short = false){
		$restore = $this->saveState();
		// Открытие
		if (!$this->matchCh('<')) return false;
		$this->skipSpaces();
		if(!$this->name($name)){
			$this->restoreState();
			return false;
		}

		// Пробуем получить список атрибутов тега
		if($this->curCh != '>' && $this->curCh != '/') $this->tagParams($params);

		// Короткая запись тега
		$short = !empty($this->tagsRules[$name][self::TR_TAG_SHORT]);

		// Short && XHTML && !Slash || Short && !XHTML && !Slash = ERROR
		$slash = $this->matchCh('/');
		//if(($short && $this->isXHTMLMode && !$slash) || (!$short && !$this->isXHTMLMode && $slash)){
		if(!$short && $slash){
			$this->restoreState();
			return false;
		}

		$this->skipSpaces();

		// Закрытие
		if(!$this->matchCh('>')) {
			//$this->restoreState($restore);
			//return false;
			$this->skipUntilStr('>');
		}

		$this->skipSpaces();
		return true;
	}


	/**
	 * Метод проверяет, дошли ли мы до закрывающего тега и, если дошли, то определяет до какого
	 *
	 * @param string $name - ссылка для сохранения названия закрывающего тега
	 * @return bool - флаг того, нашелся ли уже закрывающий тег
	 */
  protected function tagClose(&$name){
		$this->saveState();
		if(!$this->matchCh('<')) return false;
		$this->skipSpaces();
		if(!$this->matchCh('/')) {
			$this->restoreState();
			return false;
		}
		$this->skipSpaces();
		if(!$this->name($name)){
			$this->restoreState();
			return false;
		}
		$this->skipSpaces();
		if(!$this->matchCh('>')) {
			$this->restoreState();
			return false;
		}
		return true;
	}

	/**
	 * метод считывает и сохраняет параметры массива
	 *
	 * @param array $params - ссылка для сохранения параметров тега в массив
	 * @return bool - флаг того, нашелся ли хоть один параметр
	 */
	protected function tagParams(&$params = array()){
		$name = null;
		$value = null;
		while($this->tagParam($name, $value)) {
			$params[$name] = $value;
			$name = ''; $value = '';
		}
		return count($params) > 0;
	}

	/**
	 * Метод обрабатывает параметр тега и сохраняет его
	 *
	 * @param string $name - ссылка для сохранения названия параметра
	 * @param string $value - ссылка для сохранения значения параметра
	 * @return bool - флаг, удалось ли получить корректный параметр
	 */
  protected function tagParam(&$name, &$value){
    // Запоминаем состояние
    $this->saveState();
		// Получаем имя параметра. Если это неправильное имя, то заканчиваем
    if (!$this->name($name, true)) return false;

		// Проверяем параметр без знака равно
    if (!$this->matchCh('=', true)) {
			// Стремная штука - параметр без значения <input type="checkbox" checked>, <td nowrap class=b>
			if (($this->curCh == '>' || ($this->curChClass & self::LAT) == self::LAT)) {
				$value = null;
				return true;
			}
			else {
				$this->restoreState();
				return false;
			}
		}


		$quote = $this->matchChClass(self::TAG_QUOTE, true);
		// Если мы долши до style и встретили открывающую кавычку
		if ($name == 'style' && $quote) {
		  //echo '~' . $name . '~';
		  //echo '<pre>';
		    //print_r($this->states[count($this->states)-1]);
		  //echo '</pre>';
		  $styleParams = array();
		  $this->styleParams($styleParams, $quote);
  		// Если закрытие произошло не за счет кавычки
  		if ($quote && !$this->matchCh($quote, true)){
  			//$this->restoreState();
  			//return false;
  			$this->skipUntilStr($quote);
  		}
      $this->skipSpaces();
		  $styleText = '';
		  if ($styleParams) {
		    foreach ($styleParams as $styleParamName => $styleParamValue) {
		  	 $styleText .= $styleParamName . ':' . $styleParamValue . ';';
		    }
		  }
		  $value = $styleParams;

		  return true;
		}

		// Получаем значение параметра (до кавычки или до экранирующего символа
		if (!$this->tagParamValue($value, $quote)){
			$this->restoreState();
			return false;
		}

		// Если закрытие произошло не за счет кавычки
		if ($quote && !$this->matchCh($quote, true)){
			$this->restoreState();
			return false;
		}

		$this->skipSpaces();
		return true;
	}

	/**
	 * метод считывает и сохраняет значение параметра
	 *
	 * @param string $value
	 * @param mixed $quote - кавычка, которой должен закончиться параметр
	 * @return bool - флаг, удалось ли считать корректно параметр тега
	 */
	protected function tagParamValue(&$value, $quote){
		if($quote !== false){
			// Нормальный параметр с кавычкамию Получаем пока не кавычки и не конец
			$escape = false;
			while($this->curChClass && ($this->curCh != $quote || $escape)){
				$escape = false;
				// Экранируем символы HTML которые не могут быть в параметрах
				$value .= isset($this->entities1[$this->curCh]) ? $this->entities1[$this->curCh] : $this->curCh;
				// Символ ескейпа <a href="javascript::alert(\"hello\")">
				if($this->curCh == '\\') $escape = true;
				$this->getCh();
			}
		} else {
			// долбаный параметр без кавычек. получаем его пока не пробел и не > и не конец
			while($this->curChClass && !($this->curChClass & self::SPACE) && $this->curCh != '>'){
				// Экранируем символы HTML которые не могут быть в параметрах
				$value .= isset($this->entities1[$this->curCh]) ? $this->entities1[$this->curCh] : $this->curCh;
				$this->getCh();
			}
		}

		return true;
	}


	/**
	 * метод считывает и сохраняет параметры массива
	 *
	 * @param array $params - ссылка для сохранения параметров тега в массив
	 * @param mixed $quote - кавычка, которой должны закрыться стили
	 */
	protected function styleParams(&$params = array(), $quote){
		$name = null;
		$value = null;
		while($this->styleParam($name, $value, $quote)) {
			$params[$name] = $value;
			if ($this->curCh == $quote) break; // Если последний параметр закончился кавычкой
			$name = ''; $value = '';
		}
		return count($params) > 0;
	}

	/**
	 * Метод обрабатывает параметр тега и сохраняет его
	 *
	 * @param string $name - ссылка для сохранения названия параметра
	 * @param string $value - ссылка для сохранения значения параметра
	 * @param mixed $quote - кавычка, которой должны закрыться стили
	 */
  protected function styleParam(&$name, &$value, $quote){
    // Запоминаем состояние
    //$this->saveState();
		// Получаем имя параметра. Если это неправильное имя, то заканчиваем
    if (!$this->name($name, true)) return false;

		// Проверяем параметр без знака двоеточия
    if (!$this->matchCh(':', true)) {
			// Стремная штука - параметр style="display;text-decoration:none;"
			if ($this->curCh == ';') {
				$value = null;
  			$this->getCh();
	   		$this->skipSpaces();
				return true;
			}
			else {
				//$this->restoreState();
				return false;
			}
		}


		// Получаем значение параметра (до кавычки или до экранирующего символа
		if (!$this->styleParamValue($value, $quote)){
			//$this->restoreState();
			return false;
		}

		// Если закрытие произошло не за счет кавычки и не за счет ";"
		if ($this->curCh == ';' || $this->curCh == $quote) {
			$this->matchCh(';', true);
		}
		else {
			//$this->restoreState();
			return false;
		}

		// Если закрытие произошло не за счет кавычки
		//if ($quote && !$this->matchCh($quote, true)){
			//$this->restoreState();
			//return false;
		//}

		$this->skipSpaces();
		return true;
	}


	/**
	 * метод считывает и сохраняет значение параметра
	 *
	 * @param string $value
	 * @param mixed $quote - кавычка, которой должен закончиться параметр
	 */
	protected function styleParamValue(&$value, $quote){

	  // Нормальный параметр с кавычкамию Получаем пока не кавычки и не конец
		$escape = false;
		while($this->curChClass && (!($this->curCh == $quote || $this->curCh == ";") || $escape)) {
		  $escape = false;
			// Экранируем символы HTML которые не могут быть в параметрах
			$value .= isset($this->entities1[$this->curCh]) ? $this->entities1[$this->curCh] : $this->curCh;
			// Символ ескейпа <a href="javascript::alert(\"hello\")">
			if($this->curCh == '\\') $escape = true;
			$this->getCh();
		}
		return true;
	}

	/**
	 *  Получает име (тега, параметра) по принципу 1 сиивол далее цифра или символ
	 *
	 * @param string $name - передается по ссылке. Сюда сохраняется название тега/параметра
	 * @param bool $minus - флаг, допускающий или нет знак "-" в названии
	 * @return bool - флаг, получилось ли получить корректное имя
	 */
	protected function name(&$name = '', $minus = false){
		if (($this->curChClass & self::LAT) == self::LAT) {
			$name.=$this->curCh;
			$this->getCh();
		}
		else {
			return false;
		}

		while ((($this->curChClass & self::NAME) == self::NAME || ($minus && $this->curCh=='-'))) {
			$name .= $this->curCh;
			$this->getCh();
		}

		$this->skipSpaces();
		return true;
	}


	protected function text(&$text){
		$text = '';
		//$punctuation = '';
		$dash = '';
		$newLine = true;
		$newWord = true; // Возможно начало нового слова
		$url = null;
		$href = null;

		// Включено типографирование?
		$typoEnabled = true;
		//$typoEnabled = !$this->noTypoMode;

		// Первый символ может быть <, это значит что tag() вернул false
		// и < к тагу не относится
		while (($this->curCh != '<') && $this->curChClass) {
			$brCount = 0;
			$spCount = 0;
			$quote = null;
			$closed = false;
			$punctuation = null;
			$entity = null;

			$this->skipSpaces($spCount);

			// автопреобразование сущностей...
			if (!$spCount && $this->curCh == '&' && $this->htmlEntity($entity)){
				$text .= isset($this->entities2[$entity]) ? $this->entities2[$entity] : $entity;
			}
			elseif ($typoEnabled && ($this->curChClass & self::PUNCTUATUON) && $this->punctuation($punctuation)){
				// Автопунктуация выключена
				// Если встретилась пунктуация - добавляем ее
				// Сохраняем пробел перед точкой если класс следующий символ - латиница
				if($spCount && $punctuation == '.' && ($this->curChClass & self::LAT)) $punctuation = ' '.$punctuation;
				$text .= $punctuation;
				$newWord = true;
			}
			elseif ($typoEnabled && ($spCount || $newLine) && $this->curCh == '-' && $this->dash($dash)){
				// Тире
				$text .= $dash;
				$newWord = true;
			}
			elseif ($typoEnabled && ($this->curChClass & self::HTML_QUOTE) && $this->quote($spCount, $quote, $closed)){
				// Кавычки
				$this->quotesOpened += $closed ? -1 : 1;
				// Исправляем ситуацию если кавычка закрыввается раньше чем открывается
				if($this->quotesOpened < 0) {
					$closed = false;
					$this->quotesOpened = 1;
				}
				$quote = $this->makeQuote($closed, $closed ? $this->quotesOpened : $this->quotesOpened-1);
				if ($spCount) $quote = ' '.$quote;
				$text .= $quote;
				$newWord = true;
			}
			elseif ($spCount > 0){
				$text .= ' ';
				// после пробелов снова возможно новое слово
				$newWord = true;
			}
			elseif ($this->isAutoBrMode && $this->skipNL($brCount)){
				// Перенос строки
				$br = $this->br . $this->nl;
				$text.= $brCount == 1 ? $br : $br.$br;
				// Помечаем что новая строка и новое слово
				$newLine = true;
				$newWord = true;
				// !!!Добавление слова
			}
			elseif ($newWord && $this->isAutoLinkMode && ($this->curChClass & self::LAT) && $this->openedTag != 'a' && $this->url($url, $href)){
				// URL
				$text .= $this->makeTag('a' , array('href' => $href), $url, false);
			}
			elseif($this->curChClass & self::PRINATABLE){
				// Экранируем символы HTML которые нельзя сувать внутрь тега (но не те? которые не могут быть в параметрах)
				$text .= isset($this->entities2[$this->curCh]) ? $this->entities2[$this->curCh] : $this->curCh;
				$this->getCh();
				$newWord = false;
				$newLine = false;
				// !!!Добавление к слова
			} else {
				// Совершенно непечатаемые символы которые никуда не годятся
				$this->getCh();
			}
		}

		// Пробелы
		$this->skipSpaces();
		return $text != '';
	}

	/**
	 * Метод проверяет, получаем ли мы представление символа через "&" или просто встретили "&"
	 * Если это представление, то сохраняет символ представление которого формируется
	 *
	 * @param string $entityCh - указатель для сохранения символа, который представлен через "&"
	 * @return bool - флаг того, нашли ли мы соответствующее представление
	 */
	protected function htmlEntity(&$entityCh){
		if($this->curCh <> '&') return false;
		$this->saveState();
		$this->matchCh('&');
		if($this->matchCh('#')){
			$entityCode = 0;
			if(!$this->number($entityCode) || !$this->matchCh(';')){
				$this->restoreState();
				return false;
			}
			$entityCh = html_entity_decode("&#$entityCode;", ENT_COMPAT, 'UTF-8');
			return true;
		} else{
			$entityName = '';
			if(!$this->name($entityName) || !$this->matchCh(';')){
				$this->restoreState();
				return false;
			}
			$entityCh = html_entity_decode("&$entityName;", ENT_COMPAT, 'UTF-8');
			return true;
		}
	}

	/**
	 * Получает встретившееся число
	 *
	 * @param string $num - указатель для сохранения встретившегося числа
	 * @return bool - флаг, корректно ли считано число
	 */
	protected function number(&$num){
		if (!(($this->curChClass & self::NUMERIC) == self::NUMERIC)) return false;
		$num = $this->curCh;
		$this->getCh();
		while (($this->curChClass & self::NUMERIC) == self::NUMERIC) {
			$num.= $this->curCh;
			$this->getCh();
		}
		return true;
	}

	/**
	 * Метод преобразовывает троеточия и прочие к единому символу
	 *
	 * @param string $punctuation - ссылка для сохранения нового значка пунктуации
	 * @return bool - флаг
	 */
	protected function punctuation(&$punctuation){
		if(!($this->curChClass & self::PUNCTUATUON)) return false;
		$this->saveState();
		$punctuation = $this->curCh;
		$this->getCh();

		// Проверяем ... и !!! и ?.. и !..
		if($punctuation == '.' && $this->curCh == '.'){
			while($this->curCh == '.') $this->getCh();
			$punctuation = $this->dotes;
		}
		elseif($punctuation == '!' && $this->curCh == '!'){
			while ($this->curCh == '!') $this->getCh();
			$punctuation = '!!!';
		}
		elseif (($punctuation == '?' || $punctuation == '!') && $this->curCh == '.'){
			while($this->curCh == '.') $this->getCh();
			$punctuation .= '..';
		}

		// Далее идёт слово - добавляем пробел
		if($this->curChClass & self::RUS) {
			if ($punctuation != '.') $punctuation .= ' ';
			return true;
		// Далее идёт пробел, перенос строки, конец текста
		}
		elseif(($this->curChClass & self::SPACE) || ($this->curChClass & self::NL) || !$this->curChClass){
			return true;
		}
		else {
			$this->restoreState();
			return false;
		}
	}

  /**
   * Метод проверяет, не встретилось ли тире
   *
   * @param string $dash - ссылка для записи корректного представления тире
   * @return bool - флаг, встретилось ли тире в тексте
   */
	protected function dash(&$dash){
		if($this->curCh != '-') return false;
		$dash = '';
		$this->saveState();
		$this->getCh();
		// Несколько подряд
		while ($this->curCh == '-') $this->getCh();
		if (!$this->skipNL() && !$this->skipSpaces()) {
			$this->restoreState();
			return false;
		}
		$dash = $this->dash;
		return true;
	}

	/**
	 * Кавычка
	 *
	 * @param boolean $spacesBefore флаг, были ли перед кавычкой пробелы
	 * @param string $quote - ссылка для сохранения текущей кавычки
	 * @param boolean $closed - ссылка для сохранения, открывающая или закрывающая кавычка встретилась
	 * @return boolean
	 */
	protected function quote($spacesBefore,  &$quote, &$closed){
		$this->saveState();
		$quote = $this->curCh;
		$this->getCh();
		// Если не одна кавычка ещё не была открыта и следующий символ - не буква - то это нифига не кавычка
		if ($this->quotesOpened == 0 && !(($this->curChClass & self::ALPHA) || ($this->curChClass & self::NUMERIC))) {
			$this->restoreState();
			return false;
		}
		// Закрывается тогда, одна из кавычек была открыта и (до кавычки не было пробела или пробел или пунктуация есть после кавычки)
		// Или, если открыто больше двух кавычек - точно закрываем
		$closed =  ($this->quotesOpened >= 2) ||
		          (($this->quotesOpened >  0) &&
		           (!$spacesBefore || $this->curChClass & self::SPACE || $this->curChClass & self::PUNCTUATUON));
		return true;
	}

	/**
	 * Метод формирует кавычку, в зависимости от уровня вложенности и того, открывающая или закрывающая она должна быть
	 *
	 * @param bool $closed - закрывающая или открывающая кавычка
	 * @param int $level - текущий уровень вложенности кавычки
	 * @return string
	 */
	protected function makeQuote($closed, $level){
		$levels = count($this->textQuotes);
		if($level > $levels) $level = $levels;
		return $this->textQuotes[$level][$closed ? 1 : 0];
	}

  /**
   * Метод сохраняет url, если нам встрелся url
   *
   * @param string $url - ссылка для сохранения url (без http)
   * @param string $href - ссылка для сохранения href
   * @return bool - флаг, стретили ли мы url
   */
	protected function url(&$url, &$href){
		$this->saveState();
		$url = '';
		//$name = $this->name();
		//switch($name)
		$urlChMask = self::URL | self::ALPHA;

		if($this->matchStr('http://')){
			while($this->curChClass & $urlChMask){
				$url.= $this->curCh;
				$this->getCh();
			}

			if(!strlen($url)) {
				$this->restoreState();
				return false;
			}

			$href = 'http://' . $url;
			return true;
		} elseif($this->matchStr('www.')){
			while($this->curChClass & $urlChMask){
				$url.= $this->curCh;
				$this->getCh();
			}

			if(!strlen($url)) {
				$this->restoreState();
				return false;
			}

			$url = 'www.' . $url;
			$href = 'http://' . $url;
			return true;
		}
		$this->restoreState();
		return false;
	}

	/**
	 * Метод для сохранения ошибок в стек
	 *
	 * @param string $message
	 */
	protected function eror($message){
		$str = '';
		$strEnd = min($this->curPos + 8, $this->textLen);
		for($i = $this->curPos; $i < $strEnd; $i++){
			$str.=$this->textBuf[$i];
		}

		$this->errors[] = array(
			'message' => $message,
			'pos'     => $this->curPos,
			'ch'      => $this->curCh,
			'line'    => 0,
			'str'     => $str,
		);
	}


	/**
	 * Установка конфигурационного флага для одного или нескольких тегов
	 *
	 * @param array|string $tags тег(и)
	 * @param int $flag флаг
	 * @param mixed $value значеник=е флага
	 * @param boolean $createIfNoExists если тег ещё не определён - создть его
	 */
	protected function _cfgSetTagsFlag($tags, $flag, $value, $createIfNoExists = true){
		if(!is_array($tags)) $tags = array($tags);
		foreach($tags as $tag){
			if(!isset($this->tagsRules[$tag])) {
				if($createIfNoExists){
					$this->tagsRules[$tag] = array();
				} else {
					throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
				}
			}
			$this->tagsRules[$tag][$flag] = $value;
		}
	}

	/**
	 * КОНФИГУРАЦИЯ: Разрешение или запрет тегов
	 * Все не разрешённые теги считаются запрещёнными
	 * @param array|string $tags тег(и)
	 */
	public function cfgAllowTags($tags){
		$this->_cfgSetTagsFlag($tags, self::TR_TAG_ALLOWED, true);
	}

	/**
	 * КОНФИГУРАЦИЯ: Коротие теги типа <img>
	 * @param array|string $tags тег(и)
	 */
	function cfgSetTagShort($tags){
		$this->_cfgSetTagsFlag($tags, self::TR_TAG_SHORT, true, false);
	}

	/**
	 * КОНФИГУРАЦИЯ: Тег необходимо вырезать вместе с контентом (script, iframe)
	 * @param array|string $tags тег(и)
	 */
	function cfgSetTagCutWithContent($tags){
		$this->_cfgSetTagsFlag($tags, self::TR_TAG_CUT, true);
	}

	/**
	 * КОНФИГУРАЦИЯ: Добавление разрешённых параметров тега
	 * @param string $tag тег
	 * @param string|array $params разрешённые параметры
	 */
	function cfgAllowTagParams($tag, $params){
		if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
		if(!is_array($params)) $params = array($params);
		// Если ключа со списком разрешенных параметров не существует - создаём ео
		if(!isset($this->tagsRules[$tag][self::TR_PARAM_ALLOWED])) {
			$this->tagsRules[$tag][self::TR_PARAM_ALLOWED] = array();
		}
		foreach($params as $key => $value){
			if(is_string($key)){
				$this->tagsRules[$tag][self::TR_PARAM_ALLOWED][$key] = $value;
			} else {
				$this->tagsRules[$tag][self::TR_PARAM_ALLOWED][$value] = true;
			}
		}
	}

	/**
	 * КОНФИГУРАЦИЯ: Добавление разрешённых стилей тега
	 * @param string $tag тег
	 * @param string|array $styles разрешённые стили
	 */
	function cfgAllowTagStyles($tag, $styles){
		if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
		if(!is_array($styles)) $styles = array($styles);
		// Если ключа со списком разрешенных стилей не существует - создаём ео
		if(!isset($this->tagsRules[$tag][self::TR_STYLE_ALLOWED])) {
			$this->tagsRules[$tag][self::TR_STYLE_ALLOWED] = array();
		}
		foreach($styles as $key => $value){
			if(is_string($key)){
				$this->tagsRules[$tag][self::TR_STYLE_ALLOWED][$key] = $value;
			} else {
				$this->tagsRules[$tag][self::TR_STYLE_ALLOWED][$value] = true;
			}
		}
	}

	/**
	 * КОНФИГУРАЦИЯ: Добавление необходимых параметров тега
	 * @param string $tag тег
	 * @param string|array $params разрешённые параметры
	 */
	function cfgSetTagParamsRequired($tag, $params){
		if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
		if(!is_array($params)) $params = array($params);
		// Если ключа со списком разрешенных параметров не существует - создаём ео
		if(!isset($this->tagsRules[$tag][self::TR_PARAM_REQUIRED])) {
			$this->tagsRules[$tag][self::TR_PARAM_REQUIRED] = array();
		}
		foreach($params as $param){
			$this->tagsRules[$tag][self::TR_PARAM_REQUIRED][$param] = true;
		}
	}

	/* КОНФИГУРАЦИЯ: Установка тегов которые может содержать тег-контейнер
	 * @param string $tag тег
	 * @param string|array $childs разрешённые теги
	 * @param boolean $isContainerOnly тег является только контейнером других тегов и не может содержать текст
	 * @param boolean $isChildOnly вложенные теги не могут присутствовать нигде кроме указанного тега
	 */
	function cfgSetTagChilds($tag, $childs, $isContainerOnly = false, $isChildOnly = false){
		if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
		if(!is_array($childs)) $childs = array($childs);
		// Тег является контейнером и не может содержать текст
		if($isContainerOnly) $this->tagsRules[$tag][self::TR_TAG_CONTAINER] = true;
		// Если ключа со списком разрешенных тегов не существует - создаём ео
		if(!isset($this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS])) {
			$this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS] = array();
		}
		foreach($childs as $child){
			$this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS][$child] = true;
			//  Указанный тег должен сущеаствовать в списке тегов
			if(!isset($this->tagsRules[$child])) throw new Exception("Тег $child отсутствует в списке разрешённых тегов");
			if(!isset($this->tagsRules[$child][self::TR_TAG_PARENT])) $this->tagsRules[$child][self::TR_TAG_PARENT] = array();
			$this->tagsRules[$child][self::TR_TAG_PARENT][$tag] = true;
			// Указанные разрешённые теги могут находится только внтутри тега-контейнера
			if($isChildOnly) $this->tagsRules[$child][self::TR_TAG_CHILD] = true;
		}
	}

  /**
   * CONFIGURATION: Adding autoadd attributes and their values to tag
   * @param string $tag tag
   * @param string|array $params array of pairs attributeName => attributeValue
   */
  function cfgSetTagParamsAutoAdd($tag, $params){
      if(!isset($this->tagsRules[$tag])) throw new Exception("Tag $tag is missing in allowed tags list");
      if(!is_array($params)) $params = array($params);
      if(!isset($this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD])) {
          $this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD] = array();
      }
      foreach($params as $param => $value){
          $this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD][$param] = $value;
      }
  }

	/**
	 * Автозамена
	 *
	 * @param array $from с
	 * @param array $to на
	 */
	function cfgSetAutoReplace($from, $to){
		$this->autoReplace = array('from' => $from, 'to' => $to);
	}

	/**
	 * Включение или выключение режима замены новых строк на <br/>
	 *
	 * @param boolean $isAutoBrMode
	 */
	function cfgSetAutoBrMode($isAutoBrMode){
		$this->isAutoBrMode = $isAutoBrMode;
	}

	/**
	 * Включение или выключение режима автоматического определения ссылок
	 *
	 * @param boolean $isAutoLinkMode
	 */
	function cfgSetAutoLinkMode($isAutoLinkMode){
		$this->isAutoLinkMode = $isAutoLinkMode;
	}

	/**
	 * КОНФИГУРАЦИЯ: Теги в которых отключено типографирование типа <code>
	 * @param array|string $tags тег(и)
	 */
	function cfgSetTagNoTypography($tags){
		$this->_cfgSetTagsFlag($tags, self::TR_TAG_NO_TYPOGRAPHY, true, false);
	}
}

/**
 * Функция ord() для мультибайтовы строк
 *
 * @param string $c символ utf-8
 * @return int код символа
 */
function uniord($c) {
    $h = ord($c{0});
    if ($h <= 0x7F) {
        return $h;
    } else if ($h < 0xC2) {
        return false;
    } else if ($h <= 0xDF) {
        return ($h & 0x1F) << 6 | (ord($c{1}) & 0x3F);
    } else if ($h <= 0xEF) {
        return ($h & 0x0F) << 12 | (ord($c{1}) & 0x3F) << 6
                                 | (ord($c{2}) & 0x3F);
    } else if ($h <= 0xF4) {
        return ($h & 0x0F) << 18 | (ord($c{1}) & 0x3F) << 12
                                 | (ord($c{2}) & 0x3F) << 6
                                 | (ord($c{3}) & 0x3F);
    } else {
        return false;
    }
}

/**
 * Функция chr() для мультибайтовы строк
 *
 * @param int $c код символа
 * @return string символ utf-8
 */
function unichr($c) {
    if ($c <= 0x7F) {
        return chr($c);
    } else if ($c <= 0x7FF) {
        return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
    } else if ($c <= 0xFFFF) {
        return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
                                    . chr(0x80 | $c & 0x3F);
    } else if ($c <= 0x10FFFF) {
        return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
                                    . chr(0x80 | $c >> 6 & 0x3F)
                                    . chr(0x80 | $c & 0x3F);
    } else {
        return false;
    }
}
?>